<?php
/*
 * Module	: Template.php
 * Descr	: PHP Version of the KudzuASP Template Engine.
 *          : Modified tag structure for use in Wordpress.
 * Version  : 1.5.3
 */


namespace com\org\web\tpl {
	require_once("class.String.php");
	require_once("class.Reflector.php");
	require_once("class.FileUtil.php");
	require_once("class.Property.php");
	require_once("class.Parser.php");

	use com\org\web\tpl\utils\String as String;
	use com\org\web\tpl\utils\FileUtil as FileUtil;
	use com\org\web\tpl\utils\Property as Property;
	use com\org\web\tpl\utils\Reflector as Reflector;
	use com\org\web\tpl\utils\Collection as Collection;
	use com\org\web\tpl\utils\TagAttrParser as TagAttrParser;
	use com\org\web\tpl\utils\TemplateVarParser as TemplateVarParser;
	use com\org\web\tpl\utils\TemplateObjectParser as TemplateObjectParser;

	define("TEMPLATE_ENGINE_VERSION", "1.5.3");
	$_plugin = new Plugin();

	class Constant {
	//	const _RGX_OPEN_TAG 	= "/<!--\\[\\\\?[^\\]]+\\\\?\\]-->/";
	//	const _RGX_CLOSE_TAG 	= "/(\\<!--\\[\\/?)|(\\/?\\]--\\>)/";
	//	const _RGX_FLD 			= "/\\{\\{[^}]+\\}\\}/";
	//	const _RGX_SUB 			= "/\\(\\([^)]+\\)\\)/";
	//	const _FORMAL_END_TAG 	= "<!--[/";
	//	const _TERMED_TAG 		= "/]-->";
		const _RGX_OPEN_TAG 	= "/<!--\\{\\\\?[^\\}]+\\\\?\\}-->/u";
		const _RGX_CLOSE_TAG 	= "/(\\<!--\\{\\/?)|(\\/?\\}--\\>)/u";
		const _RGX_FLD 			= "/\\{\\{[^}]+\\}\\}/u";
		const _RGX_SUB 			= "/\\(\\([^)]+\\)\\)/u";
		const _FORMAL_END_TAG 	= "<!--{/";
		const _TERMED_TAG 		= "/}-->";
	}

	class PluginItem {
		private $_tags 	= NULL;
		private $_name 	= NULL;
		private $_desc 	= NULL;
		private $_file 	= NULL;
		private $_ver 	= NULL;

		public function __construct() {
			$this->_tags = array();
			$this->_name = "";
			$this->_desc = "";
			$this->_file = "";
			$this->_ver  = "0.0";
		}

		public function setTagHandler($tag, &$handler) {
			$tag = strtolower($tag);
			$this->_tags[$tag] = $handler;
		}

		public function setTagHandlerFunc($tag, $func, &$obj = NULL) {
			$handler = new TagHandler();
			$handler->setFunction($func);
			$handler->setObject($obj);
			$this->setTagHandler($tag, $handler);
		}

		public function &getTagHandler($tag) {
			$tag = strtolower($tag);
			return $this->_tags[$tag];
		}

		public function init($path, $name) {
			$this->_name = strtoupper($name);
			$this->_file = $path. $this->_name. ".php";
			require_once($this->_file);
			new $this->_name($this);
		}

		public function setTagHandlers(&$eng) {
			foreach ($this->_tags as $key => $value)
				$eng->setHandler($key, $value);
		}

		public function __destruct() {
			if (isset($this->_tags))
				unset($this->_tags);
			if (isset($this->_desc))
				unset($this->_desc);
			if (isset($this->_file))
				unset($this->_file);
			if (isset($this->_name))
				unset($this->_name);
			if (isset($this->_ver))
				unset($this->_ver);
		}
	}

	class Plugin {
		private $_plugins 	= NULL;
		private $_paths 	= NULL;
		private $_plugin 	= NULL;

	    public function __construct() {
		    $this->_plugins = array();
		    $this->_paths = array();
			array_push($this->_paths, ".\\");
		}

		private function exists($name) {
			$name = strtolower($name);
			return array_key_exists($name, $this->_plugins);
		}

		private function findPath($name) {
			$name = strtoupper($name);
			$count = count($this->_paths) - 1;
			for ($i = $count; $i >= 0; $i--) {
				$path = $this->_paths[$i] . $name . ".php";
				if (file_exists($path))
					return $this->_paths[$i];
			}
			return NULL;
		}

		public function import($name) {
			if ($this->exists($name))
				return $this->get($name);
			$path = $this->findPath($name);
			if (!isset($path))
			   	return NULL;
			$this->_plugin = new PluginItem();
			$this->_plugin->init($path, $name);
			$name = strtolower($name);
			$this->_plugins[$name] = $this->_plugin;
			return $this->_plugin;
		}

		public function &get($name) {
			$plugin = NULL;
			$name = strtolower($name);
			if (isset($this->_plugins[$name]))
				$plugin = $this->_plugins[$name];
			return $plugin;
		}

		public function setTagHandlers($name, $eng) {
			$plugin = $this->import($name);
			if ($plugin === NULL)
				return FALSE;
			$plugin->setTagHandlers($eng);
		}

		public function pathPush($path) {
			array_push($this->_paths, $path);
		}

		public function pathPop() {
			array_pop($this->_paths);
		}

		public function setPath($path) {
			$this->_paths[0] = $path;
		}

		public function __destruct() {
			if (isset($this->_plugins))
				unset($this->_plugins);
			if (isset($this->_paths))
				unset($this->_paths);
			if (isset($this->_plugin))
				unset($this->_plugin);
		}
	}

	class Writer {
		function write($text) {
			echo $text;
		}
	}

	class BufferedWriter {
		private $_content = NULL;

		public function __construct() {
			$this->_content = array();
		}

		public function write($text) {
		   	array_push($this->_content, $text);
		}

		public function getContent() {
		   	if (count($this->_content) === 0)
		   	   	return NULL;
			return implode("", $this->_content);
		}

		public function setContent($text) {
		   	$this->resetContent();
		   	$this->write($text);
		}

		public function resetContent() {
			$this->_content = array();
		}

		public function __destruct() {
			if (isset($this->_content))
				unset($this->_content);
		}
	}

	class MatchBuilder {
		private $_matches 			= array();
		private $_result  			= array();
		private $_current_offset 	= 0;
		private $_current_length 	= 0;
		private $_last_offset 		= 0;
		private $_last_length 		= 0;
		private $_next_offset 		= 0;
		private $_input 			= "";
		private $_length 			= 0;

		function getCount() {
			return count($this->_result);
		}

		function getInput() {
			return $this->_input;
		}

		public function &buildMatches($regex, $input) {
			$this->_last_offset = 0;
			$this->_last_length = 0;
			$this->_input = $input;
			$this->_length = strlen($input);

			preg_match_all($regex, $input, $this->_matches);
			$count = count($this->_matches[0]);

			for($i = 0; $i < $count; $i++) {
				$this->_next_offset = $this->_last_offset + $this->_last_length;
				$this->_current_offset = strpos($input, $this->_matches[0][$i], $this->_next_offset);
				$this->_current_length = strlen($this->_matches[0][$i]);
				if($this->_current_offset > $this->_next_offset)
					$this->addNonMatch();
				$this->addMatch();
				$this->_last_offset = $this->_current_offset;
				$this->_last_length = $this->_current_length;
			}

			$this->_next_offset = $this->_last_offset + $this->_last_length;

			if ($this->_next_offset < $this->_length) {
				$this->_current_offset = $this->_length;
				$this->addNonMatch();
			}

			return $this->_result;
		}

		function addNonMatch() {
			$res = array();
			$res["offset"] = $this->_next_offset;
			$res["length"] =  $this->_current_offset - $this->_next_offset;
			$res["matched"]  = FALSE;
			$res["matchedText"] = substr($this->_input, $res["offset"], $res["length"]);
			array_push($this->_result, $res);
		}

		function addMatch() {
			$res = array();
			$res["offset"] = $this->_current_offset;
			$res["length"] = $this->_current_length;
			$res["matched"]  = TRUE;
			$res["matchedText"] = substr($this->_input, $res["offset"], $res["length"]);
			array_push($this->_result, $res);
		}

		public function hasMatches() {
			return (count($this->_result) > 0);
		}

		public function &getMatches() {
			return $this->_result;
		}

		public function &getMatch($index) {
			return $this->_result[$index];
		}

		public function __destruct() {
			if (isset($this->_matches))
				unset($this->_matches);
			if (isset($this->_result))
				unset($this->_result);
			if (isset($this->_input))
				unset($this->_input);
		}
	}

	class TagHandler extends Property {
		private $_obj = NULL;
		private $_func = NULL;

		public function __construct() {
			$this->properties = array(
				"Object" => NULL,
				"Function" => NULL
			);
		}

		public function handleTag($node) {
			if (!isset($this->_obj)) {
				$func = $this->_func;
				$func($node);
				return;
			}

			Reflector::invokeMethod($this->_obj, $this->_func, array($node));
		}

		public function setObject($obj) {
			$this->_obj = $obj;
		}

		public function &getObject() {
			return $this->_obj;
		}

		public function setFunction($fnc) {
			$this->_func = $fnc;
		}

		public function &getFunction() {
			return $this->_func;
		}

		public function __destruct() {
			if (isset($this->_obj))
				unset($this->_obj);
			if (isset($this->_func))
				unset($this->_func);
		}
	}

	class IteratorStatus extends Property {
		private $_index = 0;
		private $_length = 0;
		private $_key = NULL;

		public function __construct() {
			$this->properties = array(
				"Index" => 0,
				"Count" => 0,
				"Length" => 0,
				"First" => FALSE,
				"Last" => FALSE,
				"Even" => FALSE,
				"Odd" => FALSE,
				"Key" => NULL
			);
		}

		public function getIndex() {
			return $this->_index;
		}

		public function setIndex($index) {
			$this->_index = $index;
		}

		public function getLength() {
			return $this->_length;
		}

		public function setLength($length) {
			$this->_length = $length;
		}

		public function getCount() {
			return ($this->_index + 1);
		}

		public function setCount($count) {
			throw new \Exception("Count is read only property.");
		}

		public function getFirst() {
			return ($this->_index === 0);
		}

		public function setFirst($first) {
			throw new \Exception("First is read only property.");
		}

		public function getLast() {
			return ($this->_index === ($this->getLength() - 1));
		}

		public function setLast($last) {
			throw new \Exception("Last is read only property.");
		}

		public function getEven() {
			return ($this->modulus(2) === 0);
		}

		public function setEven($even) {
			throw new \Exception("Even is read only property.");
		}

		public function getOdd() {
			return ($this->modulus(2) === 1);
		}

		public function setOdd($odd) {
			throw new \Exception("Odd is read only property.");
		}

		public function getKey() {
			return $this->_key;
		}

		public function setKey($key) {
			$this->_key = $key;
		}

		public function modulus($mod) {
			return ($this->getCount() % $mod);
		}
	}

	abstract class ContentParser extends Property {
		abstract public function getValue($name);
		abstract public function evalParam($name, $local = TRUE);
		abstract protected function replaceFields($text);

		protected function isObjRef($name) {
			if (strpos($name, "::") > 0 || strpos($name, "->") > 0)
				return TRUE;
			return FALSE;
		}

		public function parseVarName($name) {
			$key = strtolower($name);
			if (strcmp(substr($key, 0, 1 ), "$" ) === 0)
				$key = trim($key, "$");
			$key = TemplateVarParser::parse($key);
			return $key["name"];
		}

		protected function parseObjRef($data, $local = TRUE) {
			$data = TemplateObjectParser::parse($data);
			$trail = $data["trail"];
			$i = 0;
			if (!isset($data["className"])) {
				$trail[0] = $this->evalParam($trail[0]["info"]["name"], $local);
				$i = 1;
			}

			$len = count($trail);
			for($i; $i < $len; $i++) {
				$callee = $trail[$i];
				if ($callee["property"] === FALSE) {
					$args = $callee["info"]["args"];
					$count = count($args);
					for($j = 0; $j < $count; $j++) {
						$arg = $args[$j];
						if ($arg["var"] === TRUE)
							$arg["value"] = $this->evalParam($arg["value"], $local);
						$arg = $arg["value"];
						$args[$j] = $arg;
					}

					$callee["info"]["args"] = $args;
					$trail[$i] = $callee;
				}
			}

			$data["trail"] = $trail;
			return $data;
		}

		public function evalParamRef($name, $local = TRUE) {
			if ($this->isObjRef($name)) {
				$data = $this->parseObjRef($name);
				return Reflector::invokeTrail($data);
			}

			return $this->evalParam($name, $local);
		}
	}

	class TemplateNode extends ContentParser {
		private $_id 		= "";
		private $_start_tag = "";
		private $_stop_tag 	= "";
		private $_content 	= "";
		private $_node_text = null;
		private $_text_start_offset = -1;
		private $_text_end_offset = -1;
		private $_eval_proc = "";
		private $_nodes 	= array();
		private $_params 	= array();
		private $_values 	= array();
		private $_engine 	= null;
		private $_node_level = 0;
		private $_parent = NULL;

		public function __construct() {
			$this->properties = array(
				"Id" => NULL,
				"Engine" => NULL,
				"Content" => NULL,
				"StartTag" => NULL,
				"StopTag" => NULL,
				"NodeLevel" => 0,
				"Parent" => NULL
			);
		}

		public function getId() {
			return $this->_id;
		}

		public function setId($id) {
			$this->_id = $id;
		}

		public function &getEngine() {
			return $this->_engine;
		}

		public function setEngine(&$engine) {
			$this->_engine = $engine;
			$count = count($this->_nodes);
			for($i = 0; $i < $count; $i++) {
				$this->_nodes[$i]->setEngine($engine);
			}
		}

		public function getNodeLevel() {
			return $this->_node_level;
		}

		public function setNodeLevel($level) {
			$this->_node_level = $level;
		}

		public function &getParent() {
			return $this->_parent;
		}

		public function setParent(&$parent) {
			$this->_parent = $parent;
		}

		public function getStartTag() {
			return $this->_start_tag;
		}

		public function setStartTag($value = "") {
			$this->_start_tag = $value;
		}

		public function getStopTag() {
			return $this->_stop_tag;
		}

		public function setStopTag($value = "") {
			$this->_stop_tag = $value;
		}

		public function getContent() {
			return $this->_content;
		}

		public function setContent($value) {
			$this->_content = $value;
		}

		public function getEvalProc() {
			return $this->_eval_proc;
		}

		public function setEvalProc($value) {
			$this->_eval_proc = $value;
		}

		public function getNodeCount() {
			return count($this->_nodes);
		}

		public function addNode(&$node) {
			$node->setParent($this);
			array_push($this->_nodes, $node);
		}

		public function getTextStartOffset() {
			return $this->_text_start_offset;
		}

		public function setTextStartOffset($offset) {
			$this->_text_start_offset = $offset;
		}

		public function getTextEndOffset() {
			return $this->_text_end_offset;
		}

		public function setTextEndOffset($offset) {
			$this->_text_end_offset = $offset;
		}

		public function getNodeText() {
			if ($this->getTextEndOffset() > $this->getTextStartOffset()) {
				$length = $this->getTextEndOffset() - $this->getTextStartOffset();
				return substr($this->getEngine()->getTemplate(), $this->getTextStartOffset(), $length);
			}
			return NULL;
		}

		public function &getChildNode($nodeNameOrIndex) {
			if (is_string($nodeNameOrIndex))
				return $this->locateNode($nodeNameOrIndex);
			else
				return $this->_nodes[$nodeNameOrIndex];
		}

		public function &getChildNodes() {
			return $this->_nodes;
		}

		private function &locateNode($id) {
			$count = count($this->_nodes);
			for ($i = 0; $i < $count; $i++) {
				if (strcasecmp($id, $this->_nodes[$i]->_id) === 0)
					return $this->_nodes[$i];
			}

			$ret = NULL;
			return $ret;
		}

		public function getParamCount() {
			return count($this->_params);
		}

		public function getParam($name, $default = NULL) {
			if (!array_key_exists($name, $this->_params))
				return $default;
			return $this->_params[$name];
		}

		public function addParam($name, $value) {
			$this->_params[$name] = $value;
		}

		public function &getParams($excludes) {
			if (!isset($excludes) || (is_array($excludes) && count($excludes) === 0))
				return $this->_params;
			if (!is_array($excludes)) {
				$value = $excludes;
				$excludes = array();
				$excludes[0] = $value;
			}

			$return = array();
			foreach($this->_params as $key => $value) {
				if (!$this->existsParam($key, $excludes))
					$return[$key] = $value;
			}

			return $return;
		}

		private function existsParam($key, $keys) {
			foreach($keys as $temp) {
				if (strcasecmp($key, $temp) === 0)
					return TRUE;
			}

			return FALSE;
		}

		public function setParams($params) {
			$this->_params = $params;
		}

		public function hasParam($name, $errMsg) {
			if (!array_key_exists($name, $this->_params)) {
				return $this->appendTagError($errMsg);
			}

			return true;
		}

		private function evalProcString() {
			$handler = $this->_engine->getHandler($this->_eval_proc);
			if (is_null($handler))
				throw new \Exception("Invalid Handler: " . $this->_eval_proc);
			$handler = $this->_engine->getHandler($this->_id);
			$handler->handleTag($this);
		}

		public function evalParam($param, $local = TRUE) {
			if (!String::isVarName($param))
				throw new \Exception("Invalid parameter: parameter cannot be null or empty");
			$node = NULL;
			if ($local === TRUE)
				$node = $this;
			else
				$node = $this->getParent();

			while($node !== NULL) {
				if ($node->hasValue($param))
					return $node->getValue($param);
				$node = $node->getParent();
			}

			$val = NULL;
			if ($this->_engine->hasValue($param))
				$val = $this->_engine->getValue($param);
			else
				throw new \Exception("Invalid parameter: " . $param . " does not exist");
			return $val;
		}

		public function evalNode() {
			if ((strlen($this->_id) == 0) || (strlen($this->_eval_proc) == 0)) {
				$this->_engine->contentAppend($this->_start_tag);
				$this->_engine->contentAppend($this->_content);
				$this->evalNodes();
				$this->_engine->contentAppend($this->_stop_tag);
			}
			else {
				try {
					$this->evalProcString();
				}
				catch(\Exception $e) {
					$this->_engine->contentAppend("Exception: " . $e->getMessage());
				}
			}

			$this->replaceContentFields();
		}

		public function evalNodes() {
			$count = count($this->_nodes);
			for($i = 0; $i < $count; $i++)
				$this->_nodes[$i]->evalNode();
		}

		public function evalChildNode($name) {
			$node = $this->locateNode($name);
			if ($node === NULL)
			   	return FALSE;
			$node->evalNodes();
			return TRUE;
		}

		public function push() {
			$this->_engine->contentPush();
		}

		public function pop() {
			$this->_engine->contentAppend($this->_engine->contentPop());
		}

		public function appendText($text) {
			$node = new TemplateNode();
			$node->setNodeLevel($this->getNodeLevel() + 1);
			$node->setParent($this);
			$node->setContent($text);
			array_push($this->_nodes, $node);
		}

		function appendError($error) {
			$this->appendContent("<i><b>Error:&nbsp</b>" . $this->_eval_proc);
			if (strlen($error) > 0)
				$this->appendContent("&nbsp;" . $error);
			$this->appendContent("</i>");
		}

		function appendContent($text) {
			$this->_engine->contentAppend($text);
		}

		function appendTagError($text) {
			$this->push(true);
			$this->appendError($text);
			$this->pop(true);
			return false;
		}

		function isTrue($var) {
			return $this->_engine->isTrue($var);
		}

		public function assign($name, $value, $local = FALSE) {
			if ($local === FALSE)
				$this->_engine->assign($name, $value);
			else
				$this->putValue($name, $value);
		}

		private function putValue($name, $value) {
			$key = strtolower($name);
			$prev = NULL;
			if (isset($this->_values[$key]))
			   	$prev = $this->_values[$key];
			$this->_values[$key] = $value;
			return $prev;
		}

		public function getValue($name) {
			$key = strtolower($name);
			if (strcasecmp(substr($key, 0, 1 ), "$" ) === 0)
				$key = trim($key, "$");
			$key = TemplateVarParser::parse($key);
			$value = NULL;
			if (isset($this->_values[$key["name"]])) {
				$value = $this->_values[$key["name"]];
				$value = Reflector::invokeArray($value, $key["indice"]);
			}

			return $value;
		}

		public function hasValue($name) {
			if (!isset($this->_values) || count($this->_values) === 0)
				return FALSE;
			$key = $this->parseVarName($name);
			return array_key_exists($key, $this->_values);
		}

		protected function replaceFields($text) {
			$result = "";
			$mb = new MatchBuilder();
			$matches = &$mb->buildMatches(Constant::_RGX_FLD, $text);

			foreach ($matches as $m) {
				$key = $m["matchedText"];
				if ($m["matched"]) {
					$key = substr($key, 2, strlen($key) - 4);
					if ($this->isObjRef($key)) {
						$data = $this->parseObjRef($key);
						$key = Reflector::invokeTrail($data);
					}
					else {
						try {
							$key = $this->evalParam($key);
						}
						catch(\Exception $e) {
							$key = $e->getMessage();
						}
					}
				}

				$result = $result . $key;
			}

			unset($mb);
			return $result;
		}

		protected function replaceContentFields() {
			$this->_engine->setContent($this->replaceFields($this->_engine->getContent()));
		}

		public function __destruct() {
			if (isset($this->_params))
				unset($this->_params);
			if (isset($this->_values))
				unset($this->_values);
			if (isset($this->_nodes))
				unset($this->_nodes);
			if (isset($this->_engine))
				unset($this->_engine);
			if (isset($this->_parent))
				unset($this->_parent);
			parent::__destruct();
		}
	}

	class Compiler {
		private	$_debug 		= false;
		private $_parse_stack 	= array();
		private $_parse_level 	= 0;
		private $_file 			= 0;
		private $_writer 		= null;
		private $_parse_node_level = 0;

		public function __construct() {
			$this->_writer = new Writer();
		}

		public function setDebug($value) {
			$this->_debug = $value;
		}

		public function getDebug() {
			return $this->_debug;
		}

		public function setWriter(&$writer) {
			$this->_writer = $writer;
		}

		public function &getWriter($writer) {
			return $this->_writer;
		}

		private function initParseStack() {
			$this->_parse_stack = array();
			$node = new TemplateNode();
			$node->setId("ROOT_NODE");
			array_push($this->_parse_stack, new TemplateNode());
			$this->_parse_level = 0;
		}

		private function parsePush($node) {
			$this->_parse_level += 1;
			array_push($this->_parse_stack, $node);
		}

		private function parsePop() {
			$result = array_pop($this->_parse_stack);
			$this->_parse_level -= 1;
			return $result;
		}

		private function parsePeek() {
			return $this->_parse_stack[$this->_parse_level];
		}

		public function parse($template) {
			$this->initParseStack();
			$mb = new MatchBuilder();
			$matches = &$mb->buildMatches(Constant::_RGX_OPEN_TAG, $template);

			foreach ($matches as $m) {
				if ($m["matched"])
					$this->handleTagMatch($m);
				else
					$this->handleNodeText($m);
			}

			while ($this->_parse_level > 0) {
				$node = $this->parsePop();
				$this->parsePeek()->addNode($node);
			}

			unset($mb);
			return $this->_parse_stack[0];
		}

		private function isFormalEndTag($sTag) {
			$formal = (strpos($sTag, Constant::_FORMAL_END_TAG) === 0);
			return $formal;
		}

		private function isTermedTag($sTag) {
			$isTermed = (strrpos($sTag, Constant::_TERMED_TAG) === (strlen($sTag) - strlen(Constant::_TERMED_TAG)));
			return $isTermed;
		}

		private function handleTagMatch($match) {
			$tag = $match["matchedText"];
			if ($this->isFormalEndTag($tag)) {
				$node = &$this->parseEndTag($tag);
				$node->setTextEndOffset($match["offset"]);
			}
			elseif ($this->isTermedTag($tag)) {
				$this->parseTermedTag($tag);
			}
			else {
				$node = &$this->parseBeginTag($tag);
				$node->setTextStartOffset($match["offset"] + $match["length"]);
			}
		}

		private function handleNodeText($match) {
			$text = $match["matchedText"];
			$this->parsePeek()->appendText($text);
		}

		private function parseTagProperties($match, $node, $setId) {
			if ($this->_debug)
				$node->setStartTag($match);
			else
				$node->setStartTag();

			$temp = preg_replace(Constant::_RGX_CLOSE_TAG, "", $match);
			$temp = trim($temp);
			$tagName = NULL;
			$tagAttribs = NULL;
			$fSpaceIndex = strpos($temp, " ");
			if ($fSpaceIndex === FALSE) {
				$tagName = trim($temp);
			}
			else {
				$tagName = substr($temp, 0, $fSpaceIndex);
				$tagAttribs = substr($temp, $fSpaceIndex + 1);
			}

			if ($setId) {
				$node->setId($tagName);
				$node->setEvalProc($tagName);
				$attrs = TagAttrParser::parse($tagAttribs);
				if (is_array($attrs))
					$node->setParams($attrs);
			}
		}

		private function &parseBeginTag($match) {
			$this->_parse_node_level++;
			$node = new TemplateNode();
			$node->setNodeLevel($this->_parse_node_level);
			$this->parsePush($node);
			$this->parseTagProperties($match, $node, true);
			if ($this->_debug)
				$this->dumpParseInfo($node->getStartTag());
			return $node;
		}

		private function &parseEndTag($tag) {
			$this->_parse_node_level--;
			$temp = preg_replace(Constant::_RGX_CLOSE_TAG, "", $tag);
			$dir = explode("|", $temp);
			$id = $dir[0];
			$term = strcasecmp($id, $this->parsePeek()->getId());
			$node = null;

			while(($this->_parse_level > 0) && ($term !== 0)) {
				$node = $this->parsePop();
				$this->parsePeek()->addNode($node);
				$term = strcasecmp($id, $this->parsePeek()->getId());
			}

			$term = strcasecmp($id, $this->parsePeek()->getId());
			if ($term === 0) {
				$node = $this->parsePop();
				$node->setStopTag($tag);
				$this->parsePeek()->addNode($node);
			}
			else {
			}

			if ($this->_debug)
				$this->dumpParseInfo2($node->getStopTag());
			return $node;
		}

		private function &parseTermedTag($tag) {
			$node = new TemplateNode();
			$node->setNodeLevel($this->_parse_node_level + 1);
			$this->parseTagProperties($tag, $node, true);
			$this->parsePeek()->addNode($node);
			if ($this->_debug)
				$this->dumpParseInfo2($node->getStartTag());
			return $node;
		}

		private function dumpParseInfo2($tag) {
			$temp = "00" . ($this->_parse_level + 1);
			$temp = substr($temp, strlen($temp) - 2, 2);
			$temp .= ":";
			$temp .= str_repeat("&nbsp;&nbsp;|", $this->_parse_level + 1);
			$temp .= htmlentities($this->strTag48($tag));
			$this->_writer->write($temp. "<br/>");
		}

		private function dumpParseInfo($tag) {
			$temp = "00" . $this->_parse_level;
			$temp = substr($temp, strlen($temp) - 2, 2);
			$temp .= ":";
			$temp .= str_repeat("&nbsp;&nbsp;|", $this->_parse_level);
			$temp .= htmlentities($this->strTag48($tag));
			$this->_writer->write($temp . "<br/>");
		}

		private function strTag48($tag) {
			$len = strlen($tag);
			if ($len <= 48)
			   	return $tag;
			if ($this->isTermedTag($tag))
				return substr($tag, 0, 38) . " ... /}-->";
			return substr($tag, 0, 39) . " ... }-->";
		}

		public function __destruct() {
			if (isset($this->_parse_stack))
				unset($this->_parse_stack);
			if (isset($this->_writer))
				unset($this->_writer);
		}
	}

	class TplEngine extends ContentParser {
		private $_run_stack 	= NULL;
		private $_run_level 	= 0;
		private $_debug 		= false;
		private $_handlers 		= NULL;
		private $_iterators 	= NULL;
		private $_values 		= NULL;
		private $_root_node 	= NULL;
		private $_is_setup 		= FALSE;
		private $_writer 		= NULL;
		private $_info 			= FALSE;
		private $_start_time 	= 0;
		private $_stop_time 	= 0;
		private $_template 		= null;

		public function __construct() {
			$this->init();
			$this->_writer = new Writer();
			$this->_start_time = microtime();
			$this->_stop_time = $this->_start_time;
			$this->properties = array(
				"Values" => NULL,
				"Info" => FALSE,
				"Debug" => FALSE
			);

			$this->addPluginPath("classes/plugin/");
		}

		private function init() {
			$this->initValues();
			$this->initRuntime();
			$this->initHandlers();
			$this->_is_setup = TRUE;
		}

		private function initValues() {
			$this->_values = array();
			$this->_iterators = array();
			$this->putValue("TEMPLATE_ENGINE_VERSION", TEMPLATE_ENGINE_VERSION);
		}

		private function initRuntime() {
			$this->_run_stack = array();
			array_push($this->_run_stack, "");
			$this->_run_level = 0;
		}

		private function initHandlers() {
			$this->_handlers = array();
			$this->setHandlerFunc("CASE", "tag_CASE", $this);
			$this->setHandlerFunc("CYCLE", "tag_CYCLE", $this);
			$this->setHandlerFunc("IF", "tag_IFTHEN", $this);
			$this->setHandlerFunc("IFTRUE", "tag_IFTRUE", $this);
			$this->setHandlerFunc("IFFALSE", "tag_IFFALSE", $this);
			$this->setHandlerFunc("IGNORE", "tag_IGNORE", $this);
			$this->setHandlerFunc("IGN", "tag_IGNORE", $this);
			$this->setHandlerFunc("COMMENT", "tag_IGNORE", $this);
			$this->setHandlerFunc("CMT", "tag_IGNORE", $this);
			$this->setHandlerFunc("IMPORT", "tag_IMPORT", $this);
			$this->setHandlerFunc("FOR", "tag_FORARRAY", $this);
			$this->setHandlerFunc("FORARRAY", "tag_FORARRAY", $this);
			$this->setHandlerFunc("FOREACH", "tag_FOREACH", $this);
			$this->setHandlerFunc("ITERATE", "tag_ITERATE", $this);
			$this->setHandlerFunc("REPLACE", "tag_REPLACE", $this);
			$this->setHandlerFunc("REP", "tag_REPLACE", $this);
			$this->setHandlerFunc("LITERAL", "tag_LITERAL", $this);
			$this->setHandlerFunc("LIT", "tag_LITERAL", $this);
			$this->setHandlerFunc("ASSIGN", "tag_SETVALUE", $this);
		}

		public function getDebug() {
			return $this->_debug;
		}

		public function setDebug($value) {
			$this->_debug = $value;
			if ($this->_debug)
				$this->_writer = new BufferedWriter();
		}

		function &getWriter() {
			return $this->_writer;
		}

		function setWriter(&$writer) {
			$this->_writer = $writer;
		}

		public function &getTemplate() {
			return $this->_template;
		}

		public function getInfo() {
			return $this->_info;
		}

		public function setInfo($info) {
			$this->_info = $info;
		}

		public function &getValues() {
			return $this->_values;
		}

		public function setValues(&$values) {
			$this->_values = $values;
		}

		function setHandler($name, &$handler) {
			$key = strtolower($name);
			$prev = NULL;
			if (isset($this->_handlers[$key]))
			   	$prev = $this->_handlers[$key];
			$this->_handlers[$key] = $handler;
			return $prev;
		}

		function setHandlerFunc($name, $fnName, &$obInst = NULL) {
			$handler = new TagHandler();
			$handler->setObject($obInst);
			$handler->setFunction($fnName);
			$this->setHandler($name, $handler);
		}

		public function &getHandler($name) {
			$key = strtolower($name);
			$handler = NULL;
			if (isset($this->_handlers[$key]))
				$handler = $this->_handlers[$key];
			return $handler;
		}

		public function &getHandlers() {
			return $this->_handlers;
		}

		public function setHandlers(&$handlers) {
			$this->_handlers = $handlers;
		}

		public function addHandler($name, &$handler) {
			if ($this->hasHandler($name))
				return false;
			$this->_handlers[strtolower($name)] = $handler;
			return true;
		}

		public function hasHandler($name) {
			return array_key_exists(strtolower($name), $this->_handlers);
		}

		public function assign($name, $value) {
			return $this->putValue($name, $value);
		}

		public function setValue($name, $value) {
			return $this->putValue($name, $value);
		}

		public function getValue($name) {
			$key = strtolower($name);
			if (strcasecmp(substr($key, 0, 1 ), "$" ) === 0)
				$key = trim($key, "$");
			$key = TemplateVarParser::parse($key);
			$value = NULL;
			if (isset($this->_values[$key["name"]])) {
				$value = $this->_values[$key["name"]];
				$value = Reflector::invokeArray($value, $key["indice"]);
			}

			return $value;
		}

		public function hasValue($name) {
			if (!isset($this->_values) || count($this->_values) === 0)
				return FALSE;
			$key = $this->parseVarName($name);
			return array_key_exists($key, $this->_values);
		}

		private function putValue($name, $value) {
			$key = strtolower($name);
			$prev = NULL;
			if (isset($this->_values[$key]))
			   	$prev = $this->_values[$key];
			$this->_values[$key] = $value;
			return $prev;
		}

		private function parseFile($filePath) {
			$template = FileUtil::readFileContent($filePath);
			$this->parseString($template);
		}

		private function parseString($template) {
			$this->initRuntime();
			$T_COMPILER = new Compiler($this);
			$T_COMPILER->setWriter($this->getWriter());
			$T_COMPILER->setDebug($this->_debug);
			$node = $T_COMPILER->parse($template);
			$this->_template = $template;
			$this->_root_node = $node;
			$node->setEngine($this);
		}

		private function evalTemplate() {
			try {
				$this->_root_node->evalNode();
			}
			catch (\Exception $e) {
				$this->contentAppend("EXCEPTION: " . $e->getMessage());
			}

			$this->contentFlush();
		}

		public function show($tpl, $string = FALSE) {
			if ($string === TRUE)
				$this->parseString($tpl);
			else
				$this->parseFile($tpl);
			$this->evalTemplate();

			$this->_stop_time = microtime();
			if($this->getInfo()) {
				$this->getWriter()->write("\r\n" . "<!-- Rendered by TemplateEngine v" . TEMPLATE_ENGINE_VERSION);
				$this->getWriter()->write(", " . number_format($this->_stop_time - $this->_start_time, 4) . " seconds -->");
			}
		}

		public function parse($tpl, $string = FALSE) {
			return $this->output($tpl, $string);
		}

		private function output($tpl, $string = FALSE) {
			if($this->_debug)
				return "";
			ob_start();
			$this->show($tpl, $string);
			$out = ob_get_contents();
			ob_end_clean();
			return $out;
		}

		public function debug($tpl, $content = FALSE) {
			if ($content === TRUE) {
				if ($this->_debug) {
					$this->setWriter(new BufferedWriter());
					$this->_writer->write("<code>");
					$this->parseString($template);
					$this->_writer->write("</code>");
					$this->setContent($this->getWriter()->getContent());
				}
				else {
					$this->parseString($template);
					$this->evalTemplate();
				}
			}
			else {
				if ($this->_debug) {
					$this->setWriter(new BufferedWriter());
					$this->_writer->write("<code>");
					$this->parseFile($file);
					$this->_writer->write("</code>");
					$this->setContent($this->getWriter()->getContent());
				}
				else {
					$this->parseFile($file);
					$this->evalTemplate();
				}
			}
		}

		function getContentLevel() {
			return $this->_run_level;
		}

		public function &getContent() {
			return $this->_run_stack[$this->_run_level];
		}

		public function setContent($content) {
			$this->_run_stack[$this->_run_level] = $content;
		}

		public function contentPush() {
			array_push($this->_run_stack, "");
			$this->_run_level++;
		}

		public function contentPop() {
			$content = $this->_run_stack[$this->_run_level];
			if ($this->_run_level > 0) {
				$this->_run_level--;
				array_pop($this->_run_stack);
			}

			return $content;
		}

		public function contentFlush() {
			for ($i = 0; $i <= $this->_run_level; $i++) {
				$this->_writer->write($this->_run_stack[$i]);
				$this->_run_stack[$i] = "";
			}
		}

		public function contentAppend($content) {
			$this->setContent($this->getContent() . $content);
		}

		protected function replaceFields($text) {
			$result = "";
			$mb = new MatchBuilder();
			$matches = &$mb->buildMatches(Constant::_RGX_FLD, $text);

			foreach ($matches as $m) {
				$key = $m["matchedText"];
				if ($m["matched"]) {
					$key = substr($key, 2, strlen($key) - 4);
					if ($this->isObjRef($key)) {
						$data = $this->parseObjRef($key);
						$key = Reflector::invokeTrail($data);
					}
					else {
						$key = $this->getValue($key);
					}
				}

				$result = $result . $key;
			}

			unset($mb);
			return $result;
		}

		function replaceParams($text, $eng) {
			$result = "";
			$mb = new MatchBuilder();
			$matches = &$mb->buildMatches(Constant::_RGX_SUB, $text);

			foreach ($matches as $m) {
				$key = $m["matchedText"];
				if ($m["matched"]) {
					$key = substr($key, 2, strlen($key) - 4);
					$key = $this->getValue($key);
					$key = urlencode($key);
				}

				$result = $result . $key;
			}

			unset($mb);
			return $result;
		}

		public function replaceContentFields() {
			$this->setContent($this->replaceFields($this->getContent()));
		}

		public function evalParam($parm, $local = TRUE) {
			if (!String::isVarName($parm))
				throw new \Exception("Invalid parameter: Parameter cannot be null or empty");
			if (!$this->hasValue($parm))
				throw new \Exception("Invalid parameter: " . $parm . " does not exist");
			return $this->getValue($parm);
		}

		function importPlugin($name) {
			global $_plugin;
			$_plugin->import($name);
			$_plugin->setTagHandlers($name, $this);
		}

		function getPlugin() {
			global $_plugin;
			return $_plugin;
		}

		public function setPluginPath($path) {
			global $_plugin;
			$_plugin->setPath($path);
		}

		public function addPluginPath($path) {
			global $_plugin;
			$_plugin->pathPush($path);
		}

		public function isTrue($var) {
			if($var) {
				if(is_bool($var) || is_int($var) || is_long($var) || is_float($var))
					return TRUE;
				elseif(is_numeric($var))
					return ((float)$var) ? TRUE : FALSE;
				elseif(is_string($var))
					return !in_array(strtolower($var), array("false", "f", "no", "n"));
				elseif(is_object($var))
				   	return ((Array)$var)? TRUE : FALSE;
				return TRUE;
			}

			return FALSE;
		}

		/* * * * BUILTIN TAG HANDLERS * * * */
		private function tag_CASE ($node) {
			if(!$node->hasParam("var", "var=\"\""))
			   	return;
			$var = $node->getParam("var");
			$value = $node->evalParamRef($var, FALSE);
			$child = $node->getChildNode($value);
			if ($node->evalChildNode($value))
				return;
			$node->evalChildNode("else");
		}

		public function tag_CYCLE($node) {
			$name = $node->getParam("name");
			if (!String::isVarName($name))
				$name = "globalCycle";
			$delimiter = $node->getParam("delimiter");
			if (String::isEmpty($delimiter))
				$delimiter = ",";
			$values = explode($delimiter, $node->getParam("values"));
			$out = String::evalBool($node->getParam("out"), FALSE);
			$cycle = NULL;
			try {
				$cycle = $node->evalParam($name);
			}
			catch(\Exception $e) {
				$cycle = array();
				$cycle["index"] = 0;
				$cycle["value"] = $values[0];
			}

			if ($out) {
				$node->push();
				$node->getEngine()->contentAppend($cycle["value"]);
				$node->pop();
			}

			$cycle["index"] = $cycle["index"] + 1;
			if ($cycle["index"] < 0 || $cycle["index"] >= count($values))
				$cycle["index"] = 0;
			$cycle["value"] = $values[$cycle["index"]];
			$node->assign($name, $cycle);
		}

		private function tag_FORARRAY($node) {
			if (!$node->hasParam("from", "from=\"\""))
				return;
			$from = $node->getParam("from");
			$array = $node->evalParamRef($from, FALSE);
			if (!is_array($array)) {
				$node->appendTagError("NotAnArray: " . $from);
				return;
			}

			$var = $node->getParam("as");
			$func = $node->getParam("func");
			$stVar = $node->getParam("status");
			$obj = NULL;
			try {
				$obj = $node->evalParamRef($node->getParam("obj", NULL), FALSE);
			}
			catch(\Exception $e) {}

			if (!String::isVarName($func)) {
				$func = "OnForArray";
				$obj = $this;
			}

			$len = count($array);
			$status = NULL;
			if (String::isVarName($stVar) || String::isVarName($func)) {
				$status = new IteratorStatus();
				$status->setLength($len);
			}
			$node->push();

			for ($cur = 0; $cur < $len; $cur++) {
				$val = $array[$cur];
				if (String::isVarName($var))
					$node->assign($var, $val, TRUE);
				if (String::isVarName($stVar) || String::isVarName($func)) {
					$status->setIndex($cur);
					if (String::isVarName($stVar))
						$node->assign($stVar, $status, TRUE);
				}

				if(!isset($obj)) {
					if(!$func($node, $val, $status))
					   	break;
				}
				else {
					if(!Reflector::invokeMethod($obj, $func, array($node, $val, $status)))
					   	break;
				}

				$this->parseNodes($node);
			}

			$node->pop();
			unset($status);
		}

		public function tag_FOREACH($node) {
			if (!$node->hasParam("from", "from=\"\""))
			   	return;
			$from = $node->getParam("from");
			$array = $node->evalParamRef($from, FALSE);
			if (!is_array($array)) {
				$node->appendTagError("NotAnArray: " . $from);
				return;
			}

			$var = $node->getParam("as");
			$func = $node->getParam("func");
			$stVar = $node->getParam("status");
			$obj = NULL;
			try {
				$obj = $node->evalParamRef($node->getParam("obj", NULL), FALSE);
			}
			catch(\Exception $e) {}

			if (!String::isVarName($func)) {
				$func = "OnForEach";
				$obj = $this;
			}

			$cur = 0;
			$len = count($array);
			$status = NULL;
			if (String::isVarName($stVar) || String::isVarName($func)) {
				$status = new IteratorStatus();
				$status->setLength($len);
			}
			$node->push();

			foreach (array_keys($array) as $key) {
				$val = $array[$key];
				if (String::isVarName($var))
					$node->assign($var, $val, TRUE);
				if (String::isVarName($stVar) || String::isVarName($func)) {
					$status->setIndex($cur);
					$status->setKey($key);
					if (String::isVarName($stVar))
						$node->assign($stVar, $status, TRUE);
				}

				if (!isset($obj)) {
					if (!$func($node, $val, $status))
						break;
				}
				else {
					if(!Reflector::invokeMethod($obj, $func, array($node, $val, $status)))
						break;
				}

				$this->parseNodes($node);
				++$cur;
			}

			$node->pop();
			unset($status);
		}

		public function tag_ITERATE($node) {
			if (!$node->hasParam("from", "from=\"\""))
				return;
			$from = $node->getParam("from");
			$array = $node->evalParamRef($from, FALSE);

			$itr = NULL;
			if (is_array($array))
				$itr = new Collection($array);
			else {
				$itr = $array;
				if (!Reflector::implement($itr, "Iterator"))
					throw new \Exception($from . " must be implemented Iterator interface.");
				if (!Reflector::implement($itr, "Countable"))
					throw new \Exception($from . " must be implemented Countable interface.");
			}

			$var = $node->getParam("as");
			$func = $node->getParam("func");
			$stVar = $node->getParam("status");
			$obj = NULL;
			try {
				$obj = $node->evalParamRef($node->getParam("obj", NULL), FALSE);
			}
			catch(\Exception $e) {}

			if (!String::isVarName($func)) {
				$func = "OnIterate";
				$obj = $this;
			}

			$status = NULL;
			if (String::isVarName($stVar) || String::isVarName($func)) {
				$status = new IteratorStatus();
				$status->setLength($itr->count());
			}
			$node->push();

			while ($itr->valid()) {
				$val = $itr->current();
				if (String::isVarName($var))
					$node->assign($var, $val, TRUE);
				if (String::isVarName($stVar) || String::isVarName($func)) {
					$status->setIndex($itr->key());
					$status->setKey($itr->key());
					if (String::isVarName($stVar))
						$node->assign($stVar, $status, TRUE);
				}

				if (!isset($obj)) {
					if (!$func($node, $val, $status))
						break;
				}
				else {
					if(!Reflector::invokeMethod($obj, $func, array($node, $val, $status)))
					   	break;
				}

				$this->parseNodes($node);
				$itr->next();
			}

			$node->pop();
			unset($status);
		}

		function tag_IMPORT($node) {
			if (!$node->hasParam("plugin", "plugin=\"pluginName1[,pluginName2]*\""))
			   	return;
			$path = $node->getParam("path");
			if (!String::isEmpty($path))
				$node->getEngine()->addPluginPath($path);
			$delimiter = $node->getParam("delimiter");
			if (String::isEmpty($delimiter))
				$delimiter = ",";
			$plugin = String::trim(explode($delimiter, $node->getParam("plugin")));
			if (is_array($plugin)) {
				$count = count($plugin);
				for ($i = 0; $i < $count; $i++)
					$this->importPlugin(strtoupper(trim($plugin[$i])));
			}
		}

		function tag_IFTHEN($node) {
			if (!$node->hasParam("test", "test=\"\""))
			   	return;
			$test = $node->getParam("test");
			$bool = $node->evalParamRef($test);
			$this->parseBool($node, $bool);
		}

		function tag_IFTRUE($node) {
			if (!$node->hasParam("test", "test=\"\""))
			   	return;
			$test = $node->getParam("test");
			$bool = $node->evalParamRef($test);
			if (!$bool)
			   	return;
			$this->parseNodes($node);
		}

		private function tag_IFFALSE($node) {
			if (!$node->hasParam("test", "test=\"\""))
			   	return;
			$test = $node->getParam("test");
			$bool = $node->evalParamRef($test);
			if ($bool)
			   	return;
			$this->parseNodes($node);
		}

		private function tag_IGNORE($node) { }

		private function tag_REPLACE($node) {
			if (!$node->hasParam("by", "by=\"\""))
			   	return;
			$strip = (strcasecmp($node->getParam("strip", ""), "true") === 0);
			$node->push();
			$val = $node->evalParamRef($node->getParam("by"));
			if ($strip)
				$val = strip_tags($val);
			$node->getEngine()->contentAppend($val);
			$node->pop();
		}

		private function tag_SETVALUE($node) {
			if (!$node->hasParam("var", "var=\"\"") ||
				!$node->hasParam("value", "value=\"\""))
			   	return;
			$var = $node->getParam("var");
			$val = $node->getParam("value");
			if (String::startsWith($val, "@")) {
				$val = trim($val, "@");
				$val = $node->evalParamRef($val, FALSE);
			}
			$parent = $node->getParam("parent", 0);
			if (is_numeric($parent))
				$parent = intval($parent);
			else
				$parent = 0;
			for ($i = 0; $i < $parent && isset($node); $i++)
				$node = $node->getParent();
			if ($parent < 0)
				$node->assign($var, $val);
			else
				$node->assign($var, $val, TRUE);
		}

		private function tag_LITERAL($node) {
			$node->push();
			$node->getEngine()->setContent($node->getNodeText());
			$node->pop();
		}

		/* * * * HELPER METHODS * * * */
		private function parseBool($node, $bool) {
			$node->push();
			$node->evalChildNode($bool ? "then" : "else");
			$node->pop();
		}

		public function parseNodes($node) {
			$node->push();
			$node->evalNodes();
			$node->pop();
		}

		public final function OnForArray($node, $value, $status) {
			return TRUE;
		}

		public final function OnForEach($node, $value, $status) {
			return TRUE;
		}

		public final function OnIterate($node, $value, $status) {
			return TRUE;
		}

		private final function stripBlankLines($s) {
			return preg_replace("/^\n+|^[\t\s]*\n+/m", "", $s);
		}

		public function __destruct() {
			if (isset($this->_run_stack))
				unset($this->_run_stack);
			if (isset($this->_handlers))
				unset($this->_handlers);
			if (isset($this->_iterators))
				unset($this->_iterators);
			if (isset($this->_values))
				unset($this->_values);
			if (isset($this->_root_node))
				unset($this->_root_node);
			if (isset($this->_writer))
				unset($this->_writer);
			if (isset($this->_template))
				unset($this->_template);
			parent::__destruct();
		}
	}
}
?>